This sample shows how to write a fully asynchronous block device driver for the traditional Mac OS. The sample tries to implement the simplest possible device driver and therefore doesn’t talk to any real hardware. Instead it mounts disk images over the network.
This sample is similar to the “RAM Disk Sample” that has shipped on the developer CDs for many years. The primary difference is that the RAM Disk Sample is synchronous, while this sample is asynchronous.
Using the Sample
User Level Operation
At the user level, the sample is very easy to use. Take a DiskCopy disk image file that’s on an AppleShare file server and drop it on to the DropMounter application. The disk image should appear on the desktop. You can remove the image by dragging it to the trash.
Sample Contents
The sample comes in three parts:
• AsyncDriverSample — This is the core disk image device driver.
• DropMounter — This is a tiny application that installs the disk image device driver and mounts any disk images that were dropped on to it.
• TradDriverLoaderLib — This is a library for installing traditional Mac OS device drivers (DRVRs). This library was written to be reused by other device driver writers. If you look at no other part of this sample, you should still investigate using TradDriverLoaderLib. This sample only contains the parts of TradDriverLoaderLib that are needed for it to function. TradDriverLoaderLib is a sample in its own right, and you should get the full distribution of that sample for more details on its operation.
For a more detailed description of the files in the distribution, see the “Packing List” section towards the end of this document.
What You Need
This sample is built in CodeWarrior 11 using both the Pascal and C compilers.
How to Build
1. Open the “Metrowerks Build Script” AppleScript.
2. Run it.
3. Choose the “AsyncDriverSample” folder
Implementation Issues
How it Works
The key to getting the most out of this sample is to grok the big picture. The following is a description of what happens when you drop a disk image on to the DropMounter application.
1. The DropMounter application starts up and installs the “.AsyncDriverSample” device driver using the TradDriverLoaderLib. For a description of how that library works, see the read me that comes with the the full distribution of the TradDriverLoaderLib.
2. The DropMounter application then sends a PBControlImmed (kSecondaryInitControlCode) call to the “.AsyncDriverSample” with secondary initialisation information. Read the later section entitled “Separating Driver and Toolbox” for a description of why.
3. The DropMounter then enters a main event loop, waiting for AppleEvents to come in. Once it has handled one AppleEvent, it quits.
4. When it gets an “Open Documents” AppleEvent, it parses the list of documents and, for each document, sends a PBControlImmed (kMountImageControlCode) call to the “.AsyncDriverSample” which instructs it to mount that disk image.
5. When the device driver gets a kMountImageControlCode, it open the data fork of the disk image file using direct AFP commands. It gets the AFP/ASP session number from the AppleShare client using the technique documented in “Technote NW 16 — Borrowed AFP Sessions”:
6. When the device driver gets a read call, it issues an asynchronous AFP command to the .XPP driver to start the read and returns ioInProgress (ie 1).
7. When the AFP command completes, the .XPP driver completes to our device driver, which completes its own read command by JMPing to IODone.
Asynchronicity
If you haven’t read “Technote 1067: Sync or Swim” yet, you should. And that’s not just because I wrote it (-: The URL is
Note that this document explains why it is so important to use PBControlImmed (rather than PBControlSync) to mount disk images.
IODone
There is always a lot of confusion about how and when you should JMP to IODone versus RTSing to the Device Manager. This driver implements a two point approach:
1. The driver header glue (in “AsyncDriverMain.c”) automatically deals with the situation correctly when returning from the device driver’s main entry point. The strategy for synchronous and asynchronous requests is as follows:
• If the driver returns a result ≤ noErr, the driver header glue assumes that it has done the request in its entirety, and hence JMPs to IODone.
• If the driver returns a result of ioInProgress (ie 1), the driver header glue assumes that the driver has started the request and that the driver will organise to complete the request at some later stage (see point below for details). The driver header glue will return to the Device Manager instead of JMPing to IODone.
2. The “AsyncDriverSample.p” file contains a routine called GenericCompletion. This is a small piece of assembly language glue. The driver uses GenericCompletion as the completion routine of the AFP commands that it issues to the .XPP driver. When the .XPP driver completes, GenericCompletion is invoked. GenericCompletion calls the PascalCompletion, which decides whether the request has completed.
• If the request has completed, PascalCompletion returns a result ≤ noErr, which causes GenericCompletion to JMP to IODone.
• If the request is still in progress, PascalCompletion returns ioInProgress (ie 1), which causes GenericCompletion to RTS from the completion routine.
Note that the AsyncDriverSample always completes requests in a single XPP request, so the second case is never exercised.
Also note that the sample goes out of its way to JMP to IODone, rather than to JSR to it. While JSRing to IODone is a lot easier to code in a high-level language (or even a low-lever language like C :–), it can get you into trouble:
1. If there are multiple queued requests that you’re capable of handling synchronously, you will steadily use up stack space. This could be bad. JMPing to IODone makes sure that your stack use is minimised.
2. When writing a traditional Mac OS device driver, you must structure your code so that invoking IODone is the last thing you do for that request. Given that restriction, the only thing you can do after JSRing to IODone is return. If you doing that, you might as well JMP to IODone.
The GenericCompletion design makes it very easy to JMP to IODone instead of JSRing to it, and I strongly recommend that you use it, or something like it.
Concurrency Control
Traditional Mac OS device drivers (DRVRs) are single threaded. They are only required to handle one request at a time. While this has a negative impact on system performance, it is a great benefit to driver writers because it means that your device driver can use global data structures for the current request, and know that these data structures are guarded against two requests happening simultaneously.
This sample exploits this feature to some extent. For example, when PascalCompletion is called, it knows that the request we need to complete is the first parameter block on the driver’s Device Control Entry’s (DCE’s) dCtlQHdr because it’s impossible for two requests to be in action at the same time.
In other areas, this sample avoids using the Device Manager’s implicit concurrency control. For example, each drive queue element has its own XPPParamBlock, rather than all the drives sharing the same global XPPParamBlock. This is because these parameter blocks are sometimes used from a PBControlImmed call, which is not queued using the driver queue, and hence is not mutually excluded vis-a-vis other driver operations. To avoid worrying about this issue, I created an XPPParamBlock for each drive.
Block Device Driver and VM
This device driver does not allow VM to page off it. This is because it calls the network drivers at the bottom level, and these drivers are not suitable for implementing a paging device driver.
In the current (System 7.6) implementation, VM pages off a drive iff:
1. The disk is not ejectable.
2. The disk is HFS format.
3. The disk is not locked. This restriction is only enforced for the primary backing store as set in the Memory control panel. This system will still file map CFM PEF containers from a locked disk.
So the only way to prevent the system file mapping PowerPC binaries from the AsyncDriverSample drives is to mark the disk as ejectable.
Ejectable Disk Image Drives
Making the drives controlled by this driver ejectable leads to a curious situation where the user can eject the disk and be stuck at the “Please insert this disk” dialog forever. This sample avoids this using the following approach:
1. When the Eject call is made, the driver marks the drive as not having a disk in place and set the dNeedTime bit in the dCtlFlags.
2. When the system sends the driver a PBControlImmed (accRun), the driver walks the drive queue looking for drives belonging to it which are marked as not having a disk in place.
3. For those drives, the driver walks the system VCB list looking for a volume that has been ejected but was previously mounted on that drive.
4. If it finds such a volume, the driver posts a disk inserted event for that drive. This will remount the volume back on the drive.
5. If it doesn’t find such a volume, the driver removes the drive queue element for that drive. Note that the accRun is made at SystemTask time, so it’s safe for the driver to use the Memory Manager to dispose of the drive queue element at this time.
So, when you choose “Eject” in the Finder, step 4 is executed and the disk disappears and then reappears on the desktop. When you choose “Put Away” in the Finder, step 5 is executed and the disk is unmounted completely.
Note that this fooling around is only necessary because we have to mark the drive as ejectable to prevent VM paging from it. Most block device driver writers don’t have to worry about this because they want to support paging.
VM Immune
Because the driver do not support paging and does not have any hardware interrupt handlers, we do not need to worry about running when page faults are fatal. This means that we can set the “VM Immune” bit in dctlFlags. This bit tells the VM system that it doesn’t have to hold down any parameter blocks (or the data they point to) which are passed to this driver. This should make everything a bit faster.
Separating Driver and Toolbox
This sample makes a best effort to separate the operation of the driver from Toolbox calls. While it’s impossible to write a traditional device driver without using Toolbox calls (most drivers need to use NewPtr at least), it’s still a good idea to avoid the Toolbox as much as possible. This is because under future Mac OS systems (and indeed the native drivers present on System 7 PCI machines), drivers are not allowed to call the Toolbox at all.
For example, the .AsyncDriverSample driver does not use the Resource Manager at all, even in response to a Open request. Instead it relies on a private control call, PBControlImmed (kSecondaryInitControlCode), from the DropMounter application to access all the necessary resources. So the DropMounter application is responsible for fetching the drive and disk icons, and passing them into the .AsyncDriverSample driver using this control call.
This approach also means that the .AsyncDriverSample driver is smaller, which means fewer bytes resident in the system heap.
Handle Based Drivers
Drivers that can be called at interrupt time (or complete operation at interrupt time, which is the same thing) should not be handle based. [ie The dCtlFlags should not have the dRAMBased flag set.] This issue is documented in the read me that comes with the full distribution of the TradDriverLoaderLib.
Driver Version Number
There’s a well used but poorly documented mechanism for traditional device drivers to publish their version number. This is to place the version number in the DCE field dCtlQHdr.qFlags. This sample does this.
Packing List
This section gives a description of all the files included with this sample and what they contain:
• AsyncDriverSample.µ — A project file to build the AsyncDriverSampleDRVR file.
• AsyncDriverMain.c — A C file that contains the main line of the disk image DRVR. This code replaces Metrowerks standard driver header. For the reasoning behind that, see the comments in the file.
• AsyncDriverSample.p — The DRVR shell that surrounds the core read/write code. This shell handles most of the interfacing between the core read/write code and the Mac OS.
• DiskImageCore.p — The core read/write code for the DRVR. This provides an internal API for creating, reading and destroying DiskImageRecords, which are the core data structure of the driver.
• AsyncDriverCommon.p — Constants and types shared between the DRVR and DropMounter.
• AsyncDriverSample.rsrc — DRVR specific resources. These end up in AsyncDriverSampleDRVR, which is included by DropMounter, which reads them in and passes them to the DRVR’s secondary init routine.
• AsyncDriverSampleDRVR.out — The compiled DRVR and its resources.
• Metrowerks Build Script — An AppleScript to build the entire project.
• :DropMounter:DropMounter.µ — A project to build the DropMounter application.
• :DropMounter:DropMounter.p — The Pascal source for the DropMounter application.
• :DropMounter:DropMounter.rsrc — The resources needed by the DropMounter application.
• :DropMounter:DropMounter — The compiled DropMounter application.
• TradDriverLoaderLib — A folder containing the parts of TradDriverLoaderLib that are required for this build. If you’re interested in this technology, you should get the full distribution.
Caveats
Implicit Assumptions
This sample makes a couple of implicit assumptions:
1. That the technique described in “Technote NW 16 — Borrowed AFP Sessions”. This technique is poorly documented and is likely to stop working some time in the future. This isn’t really an issue because a) this technique is not vital to the sample, which is about writing an asynchronous driver, not stealing AFP sessions, and b) if necessary, this sample could open its own AFP session.
2. That the driver queue and VCB queue do not change at interrupt time. We walk both of these queues at SystemTask time and if they change at interrupt time this code will fail.
Why Is It In Pascal?
I prefer to program in Pascal but I recognise that Pascal sample code is not all that convenient. In this sample parts of the code are in Pascal and other parts are in C. The reasoning I used was that if I wanted you to drop the code into your driver without looking at it, I coded it in C. If I wanted the code to illustrate a concept and for you to think about the code before including it in your project, I coded it in Pascal. Hence TradDriverLoaderLib and AsyncDriverMain are in C, and the rest is in Pascal.
Credits and Version History
If you find any problems with this stuff, mail <DevSupport@apple.com> with “Attn: Quinn” as the first line of your mail and I’ll try to fix them up.
Some of the driver and mounter code — and all the cool icons! — were taken from the MungeImage application I co-wrote with Peter N Lewis.
1.0b1 was a development build.
1.0b2 was released to DTS members only.
1.0b3 incorporated feedback from DTS reviewers and the new version of TradDriverLoaderLib.
1.0b4 made the following changes:
o AsyncDriverMain.c uses routine labels in DRVR header
o AsyncDriverMain.c uses symbolic constants to set driver flags
o no longer set driver flags in DRVROpen routine, the header has the flags right